package asteroids.model;

import java.util.TreeMap;

import asteroids.Util;
import asteroids.Vector;
import be.kuleuven.cs.som.annotate.*;

/**
 * @version	1.0
 * @author 	Jomi Verwimp
 */
public abstract class GameObject implements WorldObject {

	protected GameObject(double xPos, double yPos, double xVel, double yVel, double radius, double mass) throws IllegalArgumentException {
		if (! isValidRadius(radius)) {
			throw new IllegalArgumentException("The given radius is not a valid radius!");
		}
		this.radius = radius;
		
		if (mass <= 0) {
			throw new IllegalArgumentException("The given mass must always be strictly positive!");
		}
		this.mass = mass;
		
		setPosition(new Vector(xPos,yPos));
		
		try {
			setVelocity(new Vector(xVel, yVel));
		}
		catch(IllegalArgumentException exc) {
			exc.printStackTrace();
			setVelocity(new Vector(0, 0));
		}
	
	}
	
	/**
	 * Get the position of this game object as a vector.
	 */
	@Basic
	public Vector getPosition() {
		return position;
	}
	
	/**
	 * Check whether the given position is a valid position
	 * 
	 * @param 	position
	 * 			The position to be checked.
	 * @return	False if and only if the given position means this ship
	 * 			is located (partially) outside the boundaries of the world of this ship.
	 * 			True otherwise.
	 * 		  | result ==
	 * 		  |		!( (getWorld() != null)
	 * 		  | 	&& ( (getPosition().getX() - getRadius() < 0)
	 * 		  |       || (getPosition().getX() + getRadius() > getWorld().getDimension().getX()) 
	 * 		  |       || (getPosition().getY() - getRadius() < 0)
	 * 		  |		  || (getPosition().getY() + getRadius() > getWorld().getDimension().getY()) ) )
	 */
	public boolean isValidPosition(Vector position) {
		boolean valid = true;
		if(getWorld() != null) {
			double xPos = getPosition().getX();
			double yPos = getPosition().getY();
			double r = getRadius();
			if( (xPos - r < 0) || (xPos + r > getWorld().getDimension().getX()) || (yPos - r < 0) || (yPos + r > getWorld().getDimension().getY()) ) {
				valid = false;
			}
		}
		return valid;
	}
	
	/**
	 * Set the position of this game object to the given position.
	 * 
	 * @param	position
	 * 			The new position for this game object as a vector.		
	 * @post	The new position of this game object is equal to the given position.
	 * 		  | new.getPosition() == position
	 */
	public void setPosition(Vector position) throws IllegalArgumentException {
		if(!isValidPosition(position))
			throw new IllegalArgumentException("Illegal position!");
		this.position = position;
	}
	
	/**
	 * Move this game object over the given period of time.
	 * 
	 * @param	deltaT
	 * 			The time duration during which the game object is to be moved.
	 * @effect	The position of this game object is set to the sum of the current position
	 * 			and the distance traveled during the given period of time.
	 * 		  |	new.getPosition() == this.getPosition() + this.getVelocity()*deltaT
	 * @throws	IllegalArgumentException("Negative value for time!")
	 * 			The duration of time cannot be negative.
	 *        | deltaT < 0
	 */
	public void move(double deltaT) throws IllegalArgumentException {
		if (deltaT < 0) {
			throw new IllegalArgumentException("Negative value for time!");
		}
		setPosition( Vector.add(getPosition(), Vector.scale(getVelocity(),deltaT)) );
	}

	/**
	 * Variable registering the position of the game object as a vector.
	 */
	private Vector position;
	
	/**
	 * Get the velocity of this game object as a vector.
	 */
	@Basic
	public Vector getVelocity() {
		return velocity;
	}

	/**
	 * Set the velocity of this game object to the given velocity.
	 * 
	 * @param	velocity
	 * 			The new velocity for this game object expressed in km/s.
	 * @post	If the given velocity is not null or not invalid 
	 * 			and does not exceed this game object's speed limit,
	 * 			then the new velocity is equal to the given velocity.
	 * @post	If the given velocity is not null or not invalid 
	 * 			and exceeds this game object's speed limit,
	 * 			then the new velocity is equal to the this game object's speed limit,
	 * 			with respect to the angle of the given velocity.
	 * @post	If the given velocity is null or invalid,
	 * 			then the new velocity is equal to the old velocity.
	 * 		  | if ( velocity != null ) then
	 * 		  | 	if ( velocity.getNorm() > this.getSpeedLimit() )
	 *        | 		then velocity = this.getSpeedLimit()
	 *        |		new.getVelocity() == velocity
	 *        | else new.getVelocity() == getVelocity()
	 */
	protected void setVelocity(Vector newVelocity) {
		try {
			if ( newVelocity.getNorm() > getSpeedLimit() ) {
				double givenAngle = newVelocity.getVectorAngle();
				newVelocity = new Vector(getSpeedLimit()*Math.cos(givenAngle),getSpeedLimit()*Math.sin(givenAngle));
			}
		}
		catch(Exception exc) {
			exc.printStackTrace();
			newVelocity = getVelocity();
		}
		velocity = newVelocity;
	}
	
	/**
	 * Variable registering the velocity of the game object as a vector.
	 */
	private Vector velocity;
	
	/**
	 * Get the speed limit of this game object.
	 */
	@Basic @Immutable
	public double getSpeedLimit() {
		return speedLimit;
	}
	
	/**
	 * Variable registering the speed limit that applies to every game object in km/s.
	 */
	private final double speedLimit = 300000;

	/**
	 * Return the radius of this game object expressed in km.
	 */
	@Basic @Immutable
	public double getRadius() {
		return radius;
	}
	
	/**
	 * Check whether the given radius is a valid radius for any game object.
	 * 
	 * @param	radius
	 * 			The radius to check expressed in km.
	 * @return	True if and only if the given radius is greater than zero for any game object.
	 * 		  | result == (radius > 0)
	 */
	public boolean isValidRadius(double radius) {
		return ( (radius > 0) && (radius != Double.POSITIVE_INFINITY) );
	}
	
	/**
	 * Constant reflecting the radius of this game object.
	 */
	private final double radius;
	
	/**
	 * Return the mass of this game object expressed in kg.
	 */
	@Basic @Immutable
	public double getMass() {
		return this.mass;
	}
	
	/**
	 * Constant reflecting the mass of this game object.
	 */
	private final double mass;
	
	@Basic
	public World getWorld() {
		return this.world;
	}
	
	/**
	 * @post	| new.getWorld() == world
	 */
	protected void setWorld(World world) {
		this.world = world;
	}
	
	private World world;
	
	/**
	 * @param  	obj1
	 * @param 	obj2
	 * @effect 	| double mi = this.getMass()
	 *			| double mj = other.getMass()
	 *			| Vector deltaPos = Vector.subtract(this.getPosition(), other.getPosition())
     *			| Vector deltaVel = Vector.subtract(this.getVelocity(), other.getVelocity())
	 *			| double drDotdv = Vector.dotProduct(deltaPos, deltaVel)
	 *			| double sigma = this.getRadius() + other.getRadius()
	 *			| double j = (2*mi*mj*drDotdv)/(sigma*(mi+mj))
	 *			| Vector result = Vector.scale(deltaPos, j/sigma)
	 *			| Vector resultI = Vector.scale(result, 1/mi)
	 *			| Vector resultJ = Vector.scale(result, 1/mj)
	 * 			| resultI = Vector.add(resultI, this.getVelocity())
	 *			| resultJ = Vector.subtract(resultJ, other.getVelocity())
	 *			| this.setVelocity(resultI);
	 *			| other.setVelocity(resultJ);
	 */
	public void bounceOff(GameObject other) {
		double mi = this.getMass();
		double mj = other.getMass();
		Vector deltaPos = Vector.subtract(this.getPosition(), other.getPosition());
		Vector deltaVel = Vector.subtract(this.getVelocity(), other.getVelocity());
		double drDotdv = Vector.dotProduct(deltaPos, deltaVel);
		double sigma = this.getRadius() + other.getRadius();
		double j = (2*mi*mj*drDotdv)/(sigma*(mi+mj));
		Vector result = Vector.scale(deltaPos, j/sigma);
		Vector resultI = Vector.scale(result, 1/mi);
		Vector resultJ = Vector.scale(result, 1/mj);
		resultI = Vector.add(resultI, this.getVelocity());
		resultJ = Vector.subtract(resultJ, other.getVelocity());
		
		this.setVelocity(resultI);
		other.setVelocity(resultJ);
	}
	
	/**
	 * @effect	| Vector newVel = new Vector(-getVelocity().getX(), getVelocity().getY())
     *			| setVelocity(newVel)
	 */
	public void bounceVerBound() {
		Vector newVel = new Vector(-getVelocity().getX(), getVelocity().getY());
		setVelocity(newVel);
	}
	
	/**
	 * @effect	| Vector newVel = new Vector(getVelocity().getX(), -getVelocity().getY())
				| setVelocity(newVel)
	 */
	public void bounceHorBound() {
		Vector newVel = new Vector(getVelocity().getX(), -getVelocity().getY());
		setVelocity(newVel);
	}
	
	abstract void terminate();
	
	/**
	 * Calculate and return the distance between the two given game objects.
	 * 
	 * @param	gameObj1
	 * 			First game object to calculate the distance with.
	 * @param	gameObj2
	 * 			Second game object to calculate the distance with.
	 * @return	The distance in between gameObj1 and gameObj2.
	 * 			Negative if both game objects overlap, zero if gameObj1 equals gameObj2 or
	 * 			momentarily zero if the game objects "touch", positive otherwise.
	 * 		  | if ( gameObj1.equals(gameObj2) ) 
	 * 		  | 	 result == 0
	 *        | else result == norm(gameObj2.getPosition() - gameObj1.getPosition()) - gameObj1.getRadius() - gameObj2.getRadius()
	 * @throws	IllegalArgumentException
	 * 			One or both of the two game objects reference null.
	 * 		  | (gameObj1 == null) || (gameObj2 == null)
	 */
	public double getDistanceBetween(GameObject other)
	throws IllegalArgumentException
	{	
		if (other == null)
			throw new IllegalArgumentException("Game objects null!");
		
		double centerDistance = Vector.subtract(this.getPosition(), other.getPosition()).getNorm();
		if ( Util.fuzzyEquals(centerDistance, 0) )
			return 0;
		return (centerDistance - this.getRadius() - other.getRadius());
	}
	
	/**
	 * Return whether or not the two given game objects overlap.
	 * 
	 * @param	gameObj1
	 * 			First game object to check overlap with.
	 * @param 	gameObj2
	 * 			Second game object to check overlap with.
	 * @return	True if and only if the distance between the two given game objects
	 * 			is negative or zero. False otherwise.
	 * 		  | result == (getDistanceBetween(gameObj1,gameObj2) <= 0)
	 * @throws	IllegalArgumentException
	 * 			One or both of the two game objects reference null.
	 * 		  | (gameObj1 == null) || (gameObj2 == null)
	 */
	public boolean overlap(GameObject other)
		throws IllegalArgumentException
	{
		if (other == null)
			throw new IllegalArgumentException("Game objects cannot reference null!");	
		if (this == other) {
			return false;
		}
		return (getDistanceBetween(other) <= 0);
	}
	
	/**
	 * Return the time until collision between the two given game objects,
	 * positive infinity if they never collide.
	 * 
	 * @param	gameObj1
	 * 			First game object to check time till collision with.
	 * @param 	gameObj2
	 * 			Second game object to check time till collision with.
	 * @return	The time until collision, if the two given game objects will
	 * 			collide at some point. Positive infinity otherwise.
	 * 		  | if ( ((deltaPostiion dot deltaVelocity) >= 0)
	 * 		  |   || ( (deltaPosition dot deltaVelocity)^2
	 *        |		  -(deltaVelocity^2*(deltaPosition^2 - (gameObj1.getRadius()+gameObj2.getRadius())^2)) ) )
	 *        | 	then result == +infinity
	 *        | else result ==
	 *        |				( (deltaPosition dot deltaVelocity)
	 *        | 			  +sqrt( (deltaPosition dot deltaVelocity)^2
	 *        |				    	-(deltaVelocity^2*(deltaPosition^2 - (gameObj1.getRadius()+gameObj2.getRadius())^2)) ) )
	 *        |			   /(deltaVelocity^2)
	 * @throws	IllegalArgumentException
	 * 			One or both of the two game objects reference null.
	 * 		  | (gameObj1 == null) || (gameObj2 == null)
	 */
	public double getTimeToCollision(GameObject other)
		throws IllegalArgumentException
	{
		if (other == null)
			throw new IllegalArgumentException("Game objects cannot reference null!");
		
		Vector deltaPos = Vector.subtract(this.getPosition(), other.getPosition());
		Vector deltaVel = Vector.subtract(this.getVelocity(), other.getVelocity());
		
		double drDotdv = Vector.dotProduct(deltaPos, deltaVel);
		double dvDotdv = Vector.dotProduct(deltaVel, deltaVel);
		double drDotdr = Vector.dotProduct(deltaPos, deltaPos);
		double sigmaSqr = Math.pow(this.getRadius()+other.getRadius(),2);
		
		double d = Math.pow(drDotdv,2) - dvDotdv*(drDotdr - sigmaSqr);
		
		double timeTillColl = -((drDotdv + Math.sqrt(d))/(dvDotdv));
		if ( Util.fuzzyLessThanOrEqualTo(0, drDotdv) || Util.fuzzyLessThanOrEqualTo(d, 0) ) {
			timeTillColl = Double.POSITIVE_INFINITY;
		}
		if (Util.fuzzyLessThanOrEqualTo(timeTillColl, 0)) {
			timeTillColl = 0;
		}
		return timeTillColl;
	}
	
	/**
	 * Return the position of the possible collision between the two given game objects.
	 * 
	 * @param	gameObj1
	 * 			First game object to calculate possible position of collision with.
	 * @param	gameObj2
	 * 			Second game object to calculate possible position of collision with.
	 * @return	An array holding the x- and y-coordinates of the collision point.
	 *			Null if and only if gameObj1 and gameObj2 will never collide.
	 *		  | if ( getTimeToCollision(gameObj1,gameObj2) == +infinity )
	 *		  | 	then result == null
	 *		  | else
	 *		  | 	then result == gameObj1.getPosition()+gameObj1.getVelocity()*getTimeToCollision(gameObj1, gameObj2)
	 *		  |					   +gameObj1.getRadius()
	 * @throws	IllegalArgumentException
	 * 			One or both of the two game objects reference null.
	 * 		  | (gameObj1 == null) || (gameObj2 == null)
	 */
	public Vector getCollisionPosition(GameObject other)
		throws IllegalArgumentException
	{
		if (other == null)
			throw new IllegalArgumentException("Game objects cannot reference null!");
		
		if ( ((Double)this.getTimeToCollision(other)).isInfinite() ) {
			return null;
		}
		
		double deltaT = this.getTimeToCollision(other);
		Vector collPos = Vector.add(this.getPosition(), Vector.scale(this.getVelocity(), deltaT));
		Vector deltaPos = Vector.subtract(this.getPosition(), other.getPosition());	
		double angle = Math.atan2(deltaPos.getY(), deltaPos.getX());
		Vector collisionPosition = new Vector(collPos.getX() + this.getRadius()*Math.cos(angle), collPos.getY() + this.getRadius()*Math.sin(angle));
		
		return collisionPosition;
	}
	
	/**
	 * @param  	gameObj1
	 * @param  	gameObj2
	 * @return 	result == new Collision(this, other, this.getTimeToCollision(other),this.getCollisionPosition(other)
	 * @throws	IllegalArgumentException
	 */
	public Collision getObjectCollision(GameObject other) 
			throws IllegalArgumentException
	{
		double timeToColl = this.getTimeToCollision(other);
		Vector collPos = this.getCollisionPosition(other);
		
		return new Collision(this, other, timeToColl, collPos);
	}
	
	/**
	 * @param gameObj
	 */
	public Collision getBoundaryCollision() {
		double radius = getRadius();
		double xPos = getPosition().getX();
		double yPos = getPosition().getY();
		double xVel = getVelocity().getX();
		double yVel = getVelocity().getY();
		double width = getWorld().getDimension().getX();
		double heigth = getWorld().getDimension().getY();
		HorBound horBound = new HorBound();
		VerBound verBound = new VerBound();
		
		TreeMap<Double,Collision> boundaryColls = new TreeMap<Double,Collision>();
		
		if( !Util.fuzzyEquals(xVel, 0) ) {
			if( xVel < 0 ) {
				Double time = (xPos-radius)/-xVel;
				Vector pos = new Vector(0, yPos+radius+yVel*time);
				Collision coll = new Collision(this, verBound, time, pos);
				boundaryColls.put(time, coll);
			}
			else {
				Double time = (width-xPos-radius)/xVel;
				Vector pos = new Vector(width, yPos+radius+yVel*time);
				Collision coll = new Collision(this, verBound, time, pos);
				boundaryColls.put(time, coll);
			}
		}
		if( !Util.fuzzyEquals(yVel, 0) ) {
			if( yVel < 0 ) {
				Double time = (yPos-radius)/-yVel;
				Vector pos = new Vector(0, xPos+radius+xVel*time);
				Collision coll = new Collision(this, horBound, time, pos);
				boundaryColls.put(time, coll);
			}
			else {
				Double time = (heigth-yPos-radius)/yVel;
				Vector pos = new Vector(heigth, xPos+radius+xVel*time);
				Collision coll = new Collision(this, horBound, time, pos);
				boundaryColls.put(time, coll);
			}
		}
		if(boundaryColls.size() == 0) {
			return new Collision(this, null, Double.POSITIVE_INFINITY, null);
		}
		Collision firstBoundaryColl = boundaryColls.get(boundaryColls.firstKey());
		if (Util.fuzzyLessThanOrEqualTo(boundaryColls.firstKey(),0))
			firstBoundaryColl = new Collision(this, firstBoundaryColl.getObject2(), 0, firstBoundaryColl.getCollisionPosition());
		return firstBoundaryColl;
	}
	
}
